package service

import (
	"context"
	"math"
	"time"

	"database/sql"

	"gitee.com/hp-huiw/family-expense-api/internal/constant"
	dbmodel "gitee.com/hp-huiw/family-expense-api/internal/db/model"
	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/repository"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/util"
	"github.com/samber/lo"
)

type IncomeExpenseDetailService interface {
	Query(ctx context.Context, queryArgument *dto.IncomeExpenseDetailQueryArgumentRequest) ([]dto.IncomeExpenseDetailResponse, error)
	QueryTotalAmount(ctx context.Context, accountGroupID string, queryArgument *dto.IncomeExpenseDetailQueryArgumentRequest) (float64, error)
	QueryUsedBankCardIDs(ctx context.Context, bankCardIDs []string, accountGroupID string) ([]string, error)
	QueryUsedDetailTypeNames(ctx context.Context, incomeExpenseType string, detailTypeNames []string, accountGroupID string) ([]string, error)
	QueryListWithSummary(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64) ([]dto.IncomeExpenseDetailWithSummaryResponse, error)
	QueryDetails(ctx context.Context, accountGroupID string, id string) (*dto.IncomeExpenseDetailResponse, error)
	QueryCreditCardBill(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64, bankCardID string, statementDate time.Time) ([]dto.IncomeExpenseDetailResponse, error)
	QueryCreditCardBillAmount(ctx context.Context, accountGroupID string, bankCardID string, statementDate time.Time) (float64, error)
	QueryBasicStatistics(ctx context.Context, accountGroupID string) (*dto.IncomeExpenseBasicStatisticsResponse, error)
	QueryCoordinates(ctx context.Context, accountGroupID string, year string) ([]dto.FormattedAddressCoordinate, error)
	Exists(ctx context.Context, accountGroupID string, bankCardID string) (bool, error)
	DetailTypeExists(ctx context.Context, accountGroupID string, incomeExpenseType string, detailType string) (bool, error)
	Add(ctx context.Context, data *dto.IncomeExpenseDetailRequest, accountGroupID string, username string) error
	Update(ctx context.Context, accountGroupID string, username string, data *dto.IncomeExpenseDetailRequest) error
	Delete(ctx context.Context, accountGroupID string, ids []string) error
	UpdateDetailTypeWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string, incomeExpenseType string, currentDetailType string, newDetailType string) error
	DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
}

func NewIncomeExpenseDetailService(db *sql.DB) IncomeExpenseDetailService {
	return &incomeExpenseDetailService{
		db:                            db,
		incomeExpenseDetailRepository: repository.NewIncomeExpenseDetailRepository(db),
		bankService:                   NewBankService(db),
		userService:                   NewUserService(db),
	}
}

// Queries all income/expense which associated with provided account group.
func (s *incomeExpenseDetailService) Query(ctx context.Context, queryArgument *dto.IncomeExpenseDetailQueryArgumentRequest) ([]dto.IncomeExpenseDetailResponse, error) {
	argument := s.toQueryArgument(queryArgument)
	query, err := s.incomeExpenseDetailRepository.Query(ctx, argument)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.IncomeExpenseDetail, _ int) dto.IncomeExpenseDetailResponse {
		return *ToIncomeExpenseDetailDTO(&item)
	})

	return result, nil
}

// Queries all income/expense total amount which associated with provided account group.
// return The number of the total amount.
func (s *incomeExpenseDetailService) QueryTotalAmount(ctx context.Context, accountGroupID string, queryArgument *dto.IncomeExpenseDetailQueryArgumentRequest) (float64, error) {
	argument := s.toQueryArgument(queryArgument)
	argument.AccountGroupID = accountGroupID

	return s.incomeExpenseDetailRepository.QueryTotalAmount(ctx, argument)
}

// Queries which bank card id is used by income/expense based on provided bank card id list.
// return The string list of bank card id which is used by income/expense. Any unused bank card id will be filtered.
func (s *incomeExpenseDetailService) QueryUsedBankCardIDs(ctx context.Context, bankCardIDs []string, accountGroupID string) ([]string, error) {
	return s.incomeExpenseDetailRepository.QueryUsedBankCardIDs(ctx, bankCardIDs, accountGroupID)
}

// Queries which detail type is used by income/expense based on provided income/expense type and detail type name list.
// return The string list of detail type name which is used by income/expense. Any unused detail type will be filtered.
func (s *incomeExpenseDetailService) QueryUsedDetailTypeNames(ctx context.Context, incomeExpenseType string, detailTypeNames []string, accountGroupID string) ([]string, error) {
	if len(detailTypeNames) > 0 {
		return s.incomeExpenseDetailRepository.QueryUsedDetailTypeNames(ctx, incomeExpenseType, detailTypeNames, accountGroupID)
	} else {
		return []string{}, nil
	}
}

// Queries the income/expense list and day summary for account group by id.
// return The map instance which contains keys below.
//
//	list: The List<Map<String, ?>> return by queryListByAccountGroup. Which contains
//	an extra item named daySummary which is the List<Map<String, ?>> return by queryDailySummaryList.
func (s *incomeExpenseDetailService) QueryListWithSummary(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64) ([]dto.IncomeExpenseDetailWithSummaryResponse, error) {
	// Prepares the query argument.
	queryArgument := dto.IncomeExpenseDetailQueryArgumentRequest{
		AccountGroupID: accountGroupID,
		PageSize:       pageSize,
		PageIndex:      pageIndex,
	}

	// Queries the income/expense list by accountGroupID.
	list, err := s.Query(ctx, &queryArgument)

	if err != nil {
		return nil, err
	}

	result := lo.Map(list, func(item dto.IncomeExpenseDetailResponse, _ int) dto.IncomeExpenseDetailWithSummaryResponse {
		return dto.IncomeExpenseDetailWithSummaryResponse{
			IncomeExpenseDetailResponse: item,
		}
	})

	// Gets date list from the detail list.
	dates := lo.Map(list, func(item dto.IncomeExpenseDetailResponse, _ int) time.Time {
		return *item.IncomeExpenseDate
	})

	incomeExpenseAnalysisService := NewIncomeExpenseAnalysisService(s.db)

	// Queries month summary based on the unique date list.
	monthSummaryList, err := incomeExpenseAnalysisService.QueryMonthlySummaryList(ctx, accountGroupID, dates)

	if err != nil {
		return nil, err
	}

	for _, summary := range monthSummaryList {
		// Clones the item in order to converting its date format.
		// The original item date property used to match the detail data.
		// summaryWithFormattedDate := summary

		// Adds monthSummary to the first item of each date.
		for i := range result {
			item := &result[i]

			if item.IncomeExpenseDate.Year() == summary.IncomeExpenseDate.Year() &&
				item.IncomeExpenseDate.Month() == summary.IncomeExpenseDate.Month() {
				item.MonthSummary = &summary
				break
			}
		}
	}

	// Queries day summary based on the unique date list.
	daySummaryList, err := incomeExpenseAnalysisService.QueryDailySummaryList(ctx, accountGroupID, dates)

	if err != nil {
		return nil, err
	}

	for _, summary := range daySummaryList {
		// Clones the item in order to converting its date format.
		// The original item date property used to match the detail data.
		// summaryWithFormattedDate := summary

		// Adds daySummary to the first item of each date.
		for i := range result {
			item := &result[i]

			if item.IncomeExpenseDate.Equal(summary.IncomeExpenseDate) {
				item.DaySummary = &summary
				break
			}
		}
	}

	return result, err
}

// Queries income expense details by its id.
func (s *incomeExpenseDetailService) QueryDetails(ctx context.Context, accountGroupID string, id string) (*dto.IncomeExpenseDetailResponse, error) {
	query, err := s.incomeExpenseDetailRepository.QueryDetails(ctx, accountGroupID, id)

	if err != nil {
		return nil, err
	}

	// Queries createdBy & updatedBy basic info.
	userDetails, err := s.userService.QueryBasicInfo(ctx, accountGroupID, []string{*query.CreatedByStr, *query.UpdatedByStr})

	if err != nil {
		return nil, err
	}

	createdBy := lo.Filter(userDetails, func(item dto.UserBasicInfoResponse, _ int) bool {
		return item.Username == *query.CreatedByStr
	})[0]

	query.CreatedBy = &repository.UserBasis{
		ID:        createdBy.ID,
		Username:  createdBy.Username,
		FullName:  createdBy.FullName,
		AvatarUrl: createdBy.AvatarUrl,
	}

	updatedBy := lo.Filter(userDetails, func(item dto.UserBasicInfoResponse, _ int) bool {
		return item.Username == *query.UpdatedByStr
	})[0]

	query.UpdatedBy = &repository.UserBasis{
		ID:        updatedBy.ID,
		Username:  updatedBy.Username,
		FullName:  updatedBy.FullName,
		AvatarUrl: updatedBy.AvatarUrl,
	}

	result := ToIncomeExpenseDetailDTO(query)

	return result, nil
}

// QueryCreditCardBill queries the list which matches the date and bank card.
func (s *incomeExpenseDetailService) QueryCreditCardBill(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64, bankCardID string, statementDate time.Time) ([]dto.IncomeExpenseDetailResponse, error) {
	// Queries the statement date of the credit card with its id, statement year and month.
	statementStartDate, statementEndDate := s.createCreditCardBillStatementDateRange(statementDate)

	// Prepares the query argument.
	queryArgument := dto.IncomeExpenseDetailQueryArgumentRequest{
		AccountGroupID: accountGroupID,
		BankCardID:     bankCardID,
		PageSize:       pageSize,
		PageIndex:      pageIndex,
		StartDate:      &statementStartDate,
		EndDate:        &statementEndDate,
	}

	// Queries the list which matches the date and bank card.
	return s.Query(ctx, &queryArgument)
}

// Queries the credit card bill total amount.
// return The float64 of the total amount.
func (s *incomeExpenseDetailService) QueryCreditCardBillAmount(ctx context.Context, accountGroupID string, bankCardID string, statementDate time.Time) (float64, error) {
	// Queries the statement date of the credit card with its id, statement year and month.
	statementStartDate, statementEndDate := s.createCreditCardBillStatementDateRange(statementDate)

	// Prepares the query argument.
	queryArgument := dto.IncomeExpenseDetailQueryArgumentRequest{
		BankCardID: bankCardID,
		StartDate:  &statementStartDate,
		EndDate:    &statementEndDate,
	}

	// Queries the total amount within the card statement date range.
	return s.QueryTotalAmount(ctx, accountGroupID, &queryArgument)
}

// Queries basic statistics including totalIncomeExpenseCount and totalIncomeExpenseDays.
// @return The map instance which contains keys below.
//
//	totalIncomeExpenseCount: Int of total income/expense recorded.
//	totalIncomeExpenseDays: Int of total income/expense days recorded.
func (s *incomeExpenseDetailService) QueryBasicStatistics(ctx context.Context, accountGroupID string) (*dto.IncomeExpenseBasicStatisticsResponse, error) {
	var err error
	result := &dto.IncomeExpenseBasicStatisticsResponse{}
	result.TotalIncomeExpenseCount, err = s.incomeExpenseDetailRepository.QueryTotalIncomeExpenseCount(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	result.TotalIncomeExpenseDays, err = s.incomeExpenseDetailRepository.QueryTotalIncomeExpenseDays(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	result.TotalUserCount, err = NewAccountGroupUserService(s.db).Count(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	result.TotalBankCardCount, err = NewBankCardService(s.db).Count(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	return result, nil
}

// QueryCoordinates queries coordinates by account group id and year.
func (s *incomeExpenseDetailService) QueryCoordinates(ctx context.Context, accountGroupID string, year string) ([]dto.FormattedAddressCoordinate, error) {
	query, err := s.incomeExpenseDetailRepository.QueryCoordinates(ctx, accountGroupID, year)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.FormattedAddressCoordinate, _ int) dto.FormattedAddressCoordinate {
		return dto.FormattedAddressCoordinate{
			Latitude:         item.Latitude,
			Longitude:        item.Longitude,
			FormattedAddress: item.FormattedAddress,
		}
	})

	return result, nil
}

// Checks if this bank card is used by income/expense.
func (s *incomeExpenseDetailService) Exists(ctx context.Context, accountGroupID string, bankCardID string) (bool, error) {
	return s.incomeExpenseDetailRepository.Exists(ctx, accountGroupID, bankCardID)
}

// DetailTypeExists checks if this detail type is used by income/expense.
func (s *incomeExpenseDetailService) DetailTypeExists(ctx context.Context, accountGroupID string, incomeExpenseType string, detailType string) (bool, error) {
	return s.incomeExpenseDetailRepository.DetailTypeExists(ctx, accountGroupID, incomeExpenseType, detailType)
}

// Adds a new record of IncomeExpenseDetail with posted data.
// The username used to query the account group id and saved to createdBy and updatedBy.
func (s *incomeExpenseDetailService) Add(ctx context.Context, data *dto.IncomeExpenseDetailRequest, accountGroupID string, username string) error {
	// Gets data from dto.
	model, err := s.mapToDBModel(data, username)

	if err != nil {
		return err
	}

	model.ID = util.GenerateUUIDWithoutDashes()
	model.AccountGroupID = accountGroupID
	model.CreatedBy = username
	model.DateCreated = model.LastUpdated

	return s.incomeExpenseDetailRepository.Add(ctx, model)
}

// Updates existing IncomeExpenseDetail with posted data.
func (s *incomeExpenseDetailService) Update(ctx context.Context, accountGroupID string, username string, data *dto.IncomeExpenseDetailRequest) error {
	// Gets data from dto.
	model, err := s.mapToDBModel(data, username)

	if err != nil {
		return err
	}

	model.ID = data.ID

	return s.incomeExpenseDetailRepository.Update(ctx, accountGroupID, model)
}

// Deletes the IncomeExpenseDetails with provides list of id.
func (s *incomeExpenseDetailService) Delete(ctx context.Context, accountGroupID string, ids []string) error {
	if len(ids) > 0 {
		return s.incomeExpenseDetailRepository.BatchDelete(ctx, accountGroupID, ids)
	}

	return nil
}

// UpdateDetailTypeWithTx updates account group detail type based on currenty detail type with transaction.
func (s *incomeExpenseDetailService) UpdateDetailTypeWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string, incomeExpenseType string, currentDetailType string, newDetailType string) error {
	return s.incomeExpenseDetailRepository.UpdateDetailTypeWithTx(ctx, tx, accountGroupID, incomeExpenseType, currentDetailType, newDetailType)
}

// DeleteWithTx deletes all income and expense details by account group id with transaction.
func (s *incomeExpenseDetailService) DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	return s.incomeExpenseDetailRepository.DeleteWithTx(ctx, tx, accountGroupID)
}

func (s *incomeExpenseDetailService) toQueryArgument(dto *dto.IncomeExpenseDetailQueryArgumentRequest) *repository.IncomeExpenseDetailQueryArgument {
	return &repository.IncomeExpenseDetailQueryArgument{
		PageSize:                dto.PageSize,
		PageIndex:               dto.PageIndex,
		AccountGroupID:          dto.AccountGroupID,
		Keyword:                 dto.Keyword,
		KeywordsSearchCondition: dto.KeywordsSearchCondition,
		Type:                    dto.Type,
		DetailType:              dto.DetailType,
		PaymentType:             dto.PaymentType,
		BankCardID:              dto.BankCardID,
		StartDate:               dto.StartDate,
		EndDate:                 dto.EndDate,
	}
}

// Creates the credit card statement date range.
func (*incomeExpenseDetailService) createCreditCardBillStatementDateRange(statementDate time.Time) (statementStartDate time.Time, statementEndDate time.Time) {
	// If the statement date is 15. That means:
	// The statement start date is 15th of last month.
	// The statement end date is 14th of this month.
	statementStartDate = statementDate.AddDate(0, -1, 0)
	statementEndDate = statementDate.AddDate(0, 0, -1)

	return statementStartDate, statementEndDate
}

func (*incomeExpenseDetailService) mapToDBModel(dto *dto.IncomeExpenseDetailRequest, username string) (*dbmodel.IncomeExpenseDetail, error) {
	paymentType := dto.PaymentType

	// Sets the default payment type if needed.
	if util.IsEmptyString(paymentType) {
		paymentType = constant.PaymentTypeCash
	}

	// Just in case user inputs negative number for income and positive number for expense.
	// We take the absolute value first.
	amount := math.Abs(dto.Amount)

	// If it is expense we take it as negative number.
	if dto.Type == constant.IncomeExpenseTypeExpense {
		amount = -amount
	}

	incomeExpenseDate, err := date.ParseFromLongDateTime(dto.IncomeExpenseDate)

	if err != nil {
		return nil, err
	}

	model := dbmodel.IncomeExpenseDetail{}

	if dto.BankCard != nil {
		model.BankCardID = &dto.BankCard.ID
	}

	model.DetailType = dto.DetailType
	model.PaymentType = &paymentType
	model.Amount = amount
	model.Type = dto.Type
	model.IncomeExpenseDate = incomeExpenseDate
	model.Details = dto.Details
	model.Remark = dto.Remark
	model.Province = dto.Province
	model.City = dto.City
	model.Address = dto.Address
	model.FormattedAddress = dto.FormattedAddress
	model.District = dto.District
	model.Latitude = dto.Latitude
	model.Longitude = dto.Longitude
	model.LastUpdated = date.GetCurrentDate()
	model.UpdatedBy = username

	return &model, nil
}

// Container of IncomeExpenseDetailService functions.
type incomeExpenseDetailService struct {
	db                            *sql.DB
	incomeExpenseDetailRepository repository.IncomeExpenseDetailRepository
	bankService                   BankService
	userService                   UserService
}
